#include <SFML/Graphics.hpp>
#include <cstdlib>
#include <vector>
#include <math.h>
#include <iostream>
#include <dirent.h>
//#include <GL/gl.h>

using namespace sf;
using namespace std;

struct Point{
    int16_t x;
    int16_t y;
    void rnd(uint16_t range_x, uint16_t range_y) {
        this->x = random() % range_x;
        this->y = random() % range_y;
    }
    Point(int16_t x, int16_t y) {
        this->x = x;
        this->y = y;
    }
};

struct GraphicsOptions {
    struct {
        uint16_t size;
    } cell;
    struct {
        uint8_t line_width;
        sf::Color color;
    } grid;
    struct {
        uint8_t width;
        sf::Color color;
    } border;
    struct {
        sf::Color bg;
        sf::Color snake;
        sf::Color fail_snake;
        sf::Color pause_snake;
    } color;
};

struct GameOptions {
    int x_sz;
    int y_sz;
    uint8_t food_move_percent;
    uint8_t init_len;
    uint8_t foods_count;
};

struct ControllerOptions {
    uint16_t snake_step;
    uint16_t food_step;
    uint8_t  food_move_percent;
};


struct GameField {
    uint16_t sz_x;
    uint16_t sz_y;

    bool contains(Point &point) const {
        return (point.x>=0) && (point.y>=0) && (point.x<this->sz_x) && (point.y<this->sz_y);
    }

    GameField(uint16_t sz_x, uint16_t sz_y) {
        this->sz_x = sz_x;
        this->sz_y = sz_y;
    }
};

struct ListItem {
    Point point;
    ListItem * tail{};
    void * data;

    explicit ListItem(Point p) : point(p.x, p.y) {
        this->data = nullptr;
    }
};


enum Rotate {
    _RT_NONE0,
    RT_0,
    RT_270,
    RT_180,
    RT_90,
    _RT_NONE1,
};

enum Action {
    GA_PAUSE,
    GA_MOVE_RIGHT,
    GA_MOVE_DOWN,
    GA_MOVE_LEFT,
    GA_MOVE_UP,
    GA_FAIL
};

static Rotate actionToRotation(Action action) {
    return (Rotate)action;
}


class OneDirectionList {
private:
protected:
    ListItem * head = nullptr;
public:
    explicit OneDirectionList() {
        this->head = nullptr;
    }

    ~OneDirectionList() {
        this->release();
    }


    ListItem * getHead() {
        return this->head;
    }

    void release() {
        if (this->head != nullptr) {
            ListItem *current = this->head;
            do {
                ListItem *next = current->tail;
                delete current;
                current = next;
            } while (this->head != current);
            this->head = nullptr;
        }
    }

    void add_head(ListItem * item) {
        this->add_tail(item);
        this->head = item;
    }

    void add_tail(ListItem * item) {
        if (head == nullptr) {
            head = item;
            head->tail = head;
        } else {
            item->tail = this->head->tail;
            this->head->tail = item;
        }
    }

    ListItem * find(Point point) {
        if (this->head != nullptr) {
            ListItem *current = head;
            do {
                if ((current->point.x == point.x) && (current->point.y == point.y))
                    return current;
                current = current->tail;
            } while (head != current);
        }
        return nullptr;
    }

};


class Snake: public OneDirectionList{
public:
    void init(Point &point, uint8_t size) {
        Point p = point;
        for (int i=0;i<size;i++) {
            auto item = new ListItem(p);
            item->data = (void *)Action::GA_MOVE_RIGHT;
            this->add_tail(item);
            p.x--;
        }
    }
    void move_to(Point point){
        this->head->tail->point = point;
        this->head = this->head->tail;
    }
    void expand_to(Point point){
        auto * item = new ListItem(point);
        item->tail = this->head->tail;
        this->head->tail = item;
        this->head = item;
    }
    explicit Snake(): OneDirectionList(){ }
    ~Snake() {
        OneDirectionList::~OneDirectionList();
    }
};


class Food: public OneDirectionList {
public:
    Food(): OneDirectionList(){ }
    ~Food() { OneDirectionList::~OneDirectionList(); }
};



class GameModel{
private:
    GameOptions * options;
    GameField * game_field;
    Action action = Action::GA_PAUSE;
    Sprite sprite;

    bool can_move_to(Point &new_point) {
        if (!this->game_field->contains(new_point))
            return false;
        ListItem * item = this->snake->find(new_point);
        return ((item == nullptr) || (item == this->snake->getHead()->tail));
    }

    void update_food(ListItem * item) {
        Point f(item->point.x, item->point.y);
        do {
            f.rnd(this->game_field->sz_x, this->game_field->sz_y);
        } while (this->snake->find(f) || this->food->find(f));
        item->point = f;
    }

    void new_food() {
        Point f(0, 0);
        do {
            f.rnd(this->game_field->sz_x, this->game_field->sz_y);
        } while (this->snake->find(f) || (this->food!= nullptr && this->food->find(f)));
        this->food->add_tail(new ListItem(f));
    }

public:
    Snake * snake;
    Food * food;

    void reset() {
        this->food->release();
        this->snake->release();
        Point center(this->game_field->sz_x/2, this->game_field->sz_y/2);
        this->snake->init(center, this->options->init_len);
        for (uint8_t i=0;i<this->options->foods_count;i++)
            new_food();
        this->action = Action::GA_PAUSE;
    }

    void move_food() {
        ListItem * head = this->food->getHead();
        ListItem * current = head;
        do {
            if ((random() % 100)<this->options->food_move_percent) {
                auto new_point = current->point;
                switch (random() % 4) {
                    case 0:
                        new_point.x--;
                        break;
                    case 1:
                        new_point.x++;
                        break;
                    case 2:
                        new_point.y--;
                        break;
                    case 3:
                        new_point.y++;
                        break;
                }
                if (
                        this->game_field->contains(new_point)
                        && (this->snake->find(new_point) == nullptr)
                        && (this->food->find(new_point) == nullptr)
                ){
                    current->point = new_point;
                }
            }
            current = current->tail;
        } while(head!=current);

    }

    void move_snake() {
        if (this->action==GA_PAUSE) {
        } else if (this->action==GA_FAIL) {
        } else {
            Point new_point = this->snake->getHead()->point;
            switch (this->action) {
                case GA_MOVE_UP:
                    new_point.y--;
                    break;
                case GA_MOVE_DOWN:
                    new_point.y++;
                    break;
                case GA_MOVE_LEFT:
                    new_point.x--;
                    break;
                case GA_MOVE_RIGHT:
                    new_point.x++;
                    break;
            }
            if (can_move_to(new_point)) {
                ListItem * f = this->food->find(new_point);
                if (f!= nullptr) {
                    this->snake->expand_to(new_point);
                    this->update_food(f);
                } else
                    this->snake->move_to(new_point);
                this->snake->getHead()->data = (void *)(this->action);
            } else {
                this->action = GA_FAIL;
            }
        }
    }

    void set_action(Action action) {
        this->action = action;
    }

    Action get_action() {
        return this->action;
    }

    GameField * get_field() {
        return this->game_field;
    }

    explicit GameModel(GameOptions * options) {
        this->options = options;
        this->game_field = new GameField(options->x_sz, options->y_sz);
        this->snake = new Snake();
        this->food = new Food();
        this->reset();
    }
    ~GameModel() {
        delete this->snake;
        delete this->food;
    }
};

class TiledTexture: public Texture {
public:
    bool load_tiled(const char * file_name, Vector2<float> size) {
        return this->load_tiled(file_name, int(size.x), int(size.y));
    }
    bool load_tiled(const char * file_name, uint16_t size_x, uint16_t size_y) {
        sf::Texture tmp;
        if (tmp.loadFromFile(file_name)) {
            auto size = tmp.getSize();
            uint16_t sx = int(size_x/size.x) * size.x;
            if (sx<size_x)
                sx += size.x;
            uint16_t sy = int(size_y/size.y) * size.y;
            if (sy<size_y)
                sy += size.y;
            this->create(sx, sy);
            for (uint16_t  x = 0; x < sx; x = x + size.x)
                for (uint16_t  y = 0; y < sy; y = y + size.y)
                    this->update(tmp, x, y);
            return true;
        }
        return false;
    }
};

class SnakeBody : public RectangleShape {
private:
    Texture * texture;
public:
    SnakeBody(const char * file_name, uint16_t cell_size):RectangleShape(Vector2f(cell_size, cell_size))  {
        this->texture = new Texture();
        this->texture->loadFromFile(file_name);
        this->setTexture(this->texture);
    }
    ~SnakeBody() override {
        delete this->texture;
    }
};


class GameView {
private:
    struct TransformInfo {
        GraphicsOptions * options;

        int move_x;
        int move_y;
        int rotate;
        void reset() {
            this->rotate = 0;
            this->move_x = 0;
            this->move_y = 0;
        }
        bool has_value() {
            return this->rotate || this->move_x || this->move_y;
        }

        void prepare_for(Rotate rotation) {
            switch (rotation) {
                case Rotate::RT_0:
                    break;
                case Rotate::RT_90:
                    this->rotate = -90;
                    this->move_y = options->cell.size;
                    break;
                case Rotate::RT_180:
                    this->rotate = -180;
                    this->move_x = options->cell.size;
                    this->move_y = options->cell.size;
                    break;
                case Rotate::RT_270:
                    this->rotate = -270;
                    this->move_x = options->cell.size;
                    break;
            }

        }

        void apply(Shape * shape) {
            if (this->move_x || this->move_y)
                shape->move(this->move_x, this->move_y);
            if (this->rotate)
                shape->rotate(this->rotate);
        }

        void release(Shape * shape) {
            if (this->rotate)
                shape->rotate(-this->rotate);
            if (this->move_x || this->move_y)
                shape->move(-this->move_x, -this->move_y);
            this->reset();
        }

        TransformInfo(GraphicsOptions * options) {
            this->options = options;
            this->reset();
        }
    };

    GameModel * model;
    GraphicsOptions * options;
    RenderWindow * window;

    RectangleShape * h_line;
    RectangleShape * v_line;
    RectangleShape * grid_bg;
    TiledTexture * grid_texture;
    int food_type_count;
    Texture ** foods;
    RenderStates renderState;

    SnakeBody * snake_head;
    SnakeBody * snake_tail;
    SnakeBody * snake_body;
    SnakeBody * snake_corner;

    void draw_food() {
        RectangleShape rect(Vector2f(this->options->cell.size, this->options->cell.size));
        //circle.setFillColor(Color::Blue);
        ListItem * head = this->model->food->getHead();
        ListItem * current = head;
        do {

            if (current->data == nullptr) {
                int index = random() % this->food_type_count;
                current->data = this->foods[index];
                printf("%d\n",index);
            }
            rect.setTexture((Texture*)current->data);
            rect.setPosition(
                current->point.x * (this->options->cell.size + options->grid.line_width) + options->grid.line_width,
                current->point.y * (this->options->cell.size+options->grid.line_width) + options->grid.line_width
            );
            this->window->draw(rect, renderState);
            current = current->tail;
        } while(head!=current);
    }

    void draw_grid() {
        GameField * field = model->get_field();
        this->grid_bg->setPosition(0, 0);
        this->window->draw(*this->grid_bg, renderState);

        for (uint16_t i=0;i<=field->sz_y;i++) {
            h_line->setPosition(0, i * (this->options->cell.size + options->grid.line_width));
            this->window->draw(*this->h_line, renderState);
        }
        for (uint16_t i=0;i<=field->sz_x;i++) {
            v_line->setPosition(i * (this->options->cell.size + options->grid.line_width), 0);
            this->window->draw(*this->v_line, renderState);
        }
    }

    void draw_snake() {
        //this->snake_head
        RectangleShape head_sprite(Vector2f(this->options->cell.size, this->options->cell.size));
        RectangleShape tail_sprite(Vector2f(this->options->cell.size, this->options->cell.size));
        CircleShape body_sprite((this->options->cell.size)/2, 8);
        Color color;
        switch (this->model->get_action()) {
            case GA_FAIL:
                color = this->options->color.fail_snake;
                break;
            case GA_PAUSE:
                color = this->options->color.pause_snake;
                break;
            default:
                color = Color::White;//this->options->color.pause_snake;
                break;
        }

        ListItem * head = this->model->snake->getHead();
        ListItem * current = head->tail;
        TransformInfo local_transform(this->options);
        Shape * shape = nullptr;
        do {
            Rotate rotate;
            if (current==head) {
                shape = this->snake_head;
                rotate = actionToRotation((Action)long(current->data));
            } else if (current==head->tail) {
                shape = this->snake_tail;
                rotate = actionToRotation((Action)long(current->tail->data));
            } else {
                if (current->data==current->tail->data) {
                    shape = this->snake_body;
                    rotate = actionToRotation((Action)long(current->data));
                } else {
                    shape = this->snake_corner;
                    Action cur = (Action)long(current->data);
                    Action next = (Action)long(current->tail->data);
                    if (cur==Action::GA_MOVE_RIGHT) {
                        if (next==Action::GA_MOVE_DOWN)
                            rotate = Rotate::RT_180;
                        else
                            rotate = Rotate::RT_90;
                    } else if (cur==Action::GA_MOVE_LEFT) {
                        if (next==Action::GA_MOVE_DOWN)
                            rotate = Rotate::RT_270;
                        else
                            rotate = Rotate::RT_0;
                    } else if (cur==Action::GA_MOVE_DOWN) {
                        if (next==Action::GA_MOVE_RIGHT)
                            rotate = Rotate::RT_0;
                        else
                            rotate = Rotate::RT_90;
                    } else if (cur==Action::GA_MOVE_UP) {
                        if (next==Action::GA_MOVE_RIGHT)
                            rotate = Rotate::RT_270;
                        else
                            rotate = Rotate::RT_180;
                    }
                }

            }

            if (color!=Color::Transparent)
                shape->setFillColor(color);
            shape->setPosition(
                current->point.x * (this->options->cell.size + options->grid.line_width) + options->grid.line_width,
                current->point.y * (this->options->cell.size + options->grid.line_width) + options->grid.line_width
            );
            local_transform.prepare_for(rotate);
            local_transform.apply(shape);

            this->window->draw(*shape, renderState);
            local_transform.release(shape);

            if (current==head)
                break;

            current = current->tail;
        } while(true);
    }

    void draw_border(int width) {
        GameField * filed = this->model->get_field();
        RectangleShape v_rect(Vector2f(width, (this->options->cell.size+options->grid.line_width) * filed->sz_y + width*2 + options->grid.line_width));
        RectangleShape h_rect(Vector2f((this->options->cell.size+options->grid.line_width) * filed->sz_y + width*2 + options->grid.line_width,  width));
        h_rect.setFillColor(options->border.color);
        v_rect.setFillColor(options->border.color);
        h_rect.setPosition(0, 0);
        this->window->draw(h_rect, renderState);
        h_rect.setPosition(0, (this->options->cell.size+options->grid.line_width) * filed->sz_y + width + options->grid.line_width);
        this->window->draw(h_rect, renderState);
        v_rect.setPosition(0, 0);
        this->window->draw(v_rect, renderState);
        v_rect.setPosition((this->options->cell.size+options->grid.line_width) * filed->sz_x + width + options->grid.line_width, 0);
        this->window->draw(v_rect, renderState);
    }

    void draw_field() {
        this->draw_grid();
        this->draw_snake();
        this->draw_food();
    }

public:
    void update() {
        this->window->clear(this->options->color.bg);
        //GameField * fileld = this->model->get_field();
        this->draw_border(options->border.width);
        this->renderState.transform.translate(options->border.width, options->border.width);
        this->draw_field();
        this->renderState.transform.translate(-options->border.width, -options->border.width);
        this->window->display();
    }

    void close_window() {
        this->window->close();
    }

    GameView(GameModel * model, GraphicsOptions * options) {
        this->model = model;
        this->options = options;
        GameField * field = model->get_field();
        this->window = new RenderWindow(
                VideoMode(
                        field->sz_x * (options->cell.size + options->grid.line_width) + options->grid.line_width + options->border.width * 2,
                        field->sz_y * (options->cell.size + options->grid.line_width) + options->grid.line_width + options->border.width * 2)
                , "Python");

        this->h_line = new RectangleShape(
                Vector2f(field->sz_x * (this->options->cell.size + options->grid.line_width) + options->grid.line_width, options->grid.line_width)
        );
        this->v_line = new RectangleShape(
                Vector2f(options->grid.line_width,field->sz_y * (this->options->cell.size + options->grid.line_width) + options->grid.line_width)
        );
        this->h_line->setFillColor(this->options->grid.color);
        this->v_line->setFillColor(this->options->grid.color);

        auto bg_size = Vector2f(
                field->sz_x * (this->options->cell.size + options->grid.line_width) + options->grid.line_width,
                field->sz_y * (this->options->cell.size + options->grid.line_width) + options->grid.line_width
        );
        this->grid_bg = new RectangleShape(bg_size);
        this->grid_texture = new TiledTexture();
        this->grid_texture->load_tiled("res/field_bg.png", bg_size);
        //this->grid_texture->load_tiled("res/bricks2.png");
        this->grid_bg->setTextureRect(Rect(0, 0, int(bg_size.x), int(bg_size.y)));
        this->grid_bg->setTexture(this->grid_texture);


        auto is_valid_entry = [](dirent * entry) -> bool {
            return entry->d_name[0]!='.';
        };

        //Можно динамически определить сколько картинок в папке, столько и создать текстур
        this->food_type_count = 0;
        char file_name[512];
        DIR * dir = opendir("res/foods/");
        while (dirent * entry = readdir(dir)) {
            if (is_valid_entry(entry))
                this->food_type_count++;
        }
        rewinddir(dir);
        this->foods = (Texture**)malloc(sizeof(void*) * this->food_type_count);
        int i = 0;
        while (dirent * entry = readdir(dir)) {
            if (is_valid_entry(entry)) {
                sprintf(file_name, "res/foods/%s", entry->d_name);
                this->foods[i] = new Texture();
                this->foods[i]->loadFromFile(file_name);
                i++;
            }
        }
        closedir(dir);


        this->snake_head = new SnakeBody("res/snake/head.png", options->cell.size);
        this->snake_tail = new SnakeBody("res/snake/tail.png", options->cell.size);
        this->snake_body = new SnakeBody("res/snake/body.png", options->cell.size);
        this->snake_corner = new SnakeBody("res/snake/corner.png", options->cell.size);
    }

    ~GameView() {
        delete this->h_line;
        delete this->v_line;
        delete this->grid_bg;
        delete this->grid_texture;
        for(int i=0;i<this->food_type_count;i++)
            delete this->foods[i];
        free(this->foods);
        delete this->snake_head;
        delete this->snake_tail;
        delete this->snake_body;
        delete this->snake_corner;
    }

    bool check_close() {
        if (!this->window->isOpen())
            return true;
        Event event{};
        while (this->window->pollEvent(event)) {
            if (event.type == Event::Closed) {
                this->window->close();
                return true;
            }
        }
        return false;
    }
};


class GameController{
private:
    GameModel * model;
    ControllerOptions * controllerOptions;
    sf::Clock snake_clock;
    sf::Clock food_clock;
    bool _want_exit = false;
    void check_keys() {
        if (this->model->get_action()==Action::GA_FAIL) {
            if (sf::Keyboard::isKeyPressed(sf::Keyboard::Space))
                this->model->reset();
        } else {
            if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) {
                this->model->set_action(Action::GA_MOVE_LEFT);
            } else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) {
                this->model->set_action(Action::GA_MOVE_RIGHT);
            } else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up)) {
                this->model->set_action(Action::GA_MOVE_UP);
            } else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Down)) {
                this->model->set_action(Action::GA_MOVE_DOWN);
            }
            if (sf::Keyboard::isKeyPressed(sf::Keyboard::Space))
                this->model->set_action(Action::GA_PAUSE);
        }


        if (sf::Keyboard::isKeyPressed(sf::Keyboard::Escape))
            this->_want_exit = true;
    }
public:
    bool want_exit() const {
        return this->_want_exit;
    }
    bool tick() {
        this->check_keys();
        bool res = false;
        if (this->snake_clock.getElapsedTime().asMilliseconds()>controllerOptions->snake_step) {
            this->model->move_snake();
            this->snake_clock.restart();
            res = true;
        }
        if (this->food_clock.getElapsedTime().asMilliseconds()>controllerOptions->food_step) {
            this->model->move_food();
            this->food_clock.restart();
            res = true;
        }

        return res;
    }

    GameController(GameModel * model, ControllerOptions * controllerOptions) {
        this->model = model;
        this->controllerOptions = controllerOptions;
    }
};


int main() {
    GameOptions gameOptions = {
        .x_sz = 20,
        .y_sz = 20,
        .food_move_percent = 30,
        .init_len = 4,
        .foods_count = 10
    };
    GraphicsOptions graphicsOptions = {
        .cell={.size=32},
        .grid={
            .line_width=1,
            .color = Color(32, 64, 32),
        },
        .border={
            .width = 20,
            .color = Color::Magenta,
        },
        .color={
            .bg = Color::Black,
            .snake = Color::Green,
            .fail_snake = Color::Red,
            .pause_snake = Color(128, 128, 128),
        }
    };
    ControllerOptions controllerOptions {
        .snake_step = 300,
        .food_step = 100,
        .food_move_percent = 30,
    };
    auto * model = new GameModel(&gameOptions);
    auto * view = new GameView(model, &graphicsOptions);
    auto * controller = new GameController(model, &controllerOptions);
    while (!view->check_close()) {
        if (controller->tick()) {
            if (controller->want_exit()) {
                view->close_window();
            } else {
                view->update();
            }
        }
    }
    delete model;
    delete view;
    delete controller;
}
